/******************************************************
Cours: GTI310
Session: A2014
Groupe: 01
Projet: Laboratoire #2
etudiant(e)s: Fredy Alexander Bonilla et Émile Robinson

Professeur : Jean-François Caron
Nom du fichier: WaveFilter.java
Date cree: 2014-10-4
Date dern. modif. 2014-10-16
 *******************************************************
Historique des modifications
 *******************************************************
2014-10-09 Version 1.0
 *******************************************************/

package gti310.tp2.audio;
import java.nio.ByteBuffer;

import gti310.tp2.io.FileSink;
import gti310.tp2.io.FileSource;

/**
 * La classe WaveFilter se charche de la validation d'un fichier WAV et filtrer
 * ce dernier pour le rééchantillionner de 16khz à 8khz. 
 * 
 * @author Fredy Alexander Bonilla et Émile Robinson
 * @date 02-10-2014
 *	
 */
public class WaveFilter implements AudioFilter{

	//Constante utilisé pour effectuer la comparaison des bits
	private final static int MASK = 0xFF;

	//Valeur du format PCM dans le header d'un fichier wav
	private final static int PCM = 1;
	
	//Valeur de la fréquence du fichier d'entréé
	private final static double SAMPLE_RATE = 44100;

	//Object FileSource pour obtenir les bytes du fichier wav
	private FileSource file;
	private FileSink out;

	//Section du Bloc Type	
	private char[] tag1;
	private byte[] size1=null;
	private char[] format;

	//Section du bloc Description
	private char[] tag2;
	private Liste listeFormat = null;

	//Section du bloc Data
	private char[] tag3;
	private byte[] size3=null;
	private byte[] listeDonnee = null;

	//Nouvelles donmnées de ré-échantillonage
	private double nouvelleFrequence = 8000;
	private int nouveaubyteRate = 0;
	private byte[] nouvelleDonnee = null;
	private int nouvelleTaille = 0;

	/**
	 * Constructeur de la classe WaveFilter. Elle recoit en parametre une les 
	 * objets des classes FileSource et FileSink qui permettront d'obtenir les 
	 * tableau de octets nécessaires au filtrage et ensuite de les écrire dans 
	 * un nouveau fichier.
	 * 
	 * @param in
	 * @param out
	 */
	public WaveFilter(FileSource in, FileSink out){
		file = in;
		this.out = out;
		this.listeFormat = new Liste();		
	}

	/**
	 * Filter the input data.
	 * The function should make sure the input data is valid beforehand.
	 */
	public void process() {
		
		//Extraction des données du fichier depuis FileSource
		popData();	
		
		//validation d'un fichier au format PCM et de la fréquence à 44100 kHz		
		if(wavValidator(listeFormat.getElement(1),listeFormat.getElement(3))){
			
			System.out.println("Le fichier est de format PCM et de 44100 kHz");
			
			//Conversation du SampleRate
			conversion();
			System.out.println("Conversion terminée");
			
			//Mise à jour des données
			updateDataList();
			
			//Envoie et écriture des nouvelles données vers FileSink
			pushData();
			System.out.println("Écriture terminée");
		}

	}		
	
	/************************************************
	 * Méthode
	 ************************************************/

	/**
	 * La fonction valide l'entete du fichier reçu. Retourne true si le format
	 * en entier reçu est PCM (égale à 1); 
	 * 
	 * @param format
	 * @return
	 */
	private boolean wavValidator(byte[] format, byte[] rate){

		int formatPCM = convert2Int(format,format.length);
		int sampleRate = convert2Int(rate,rate.length);
		boolean wavValide = false;
		
		if(formatPCM == PCM && sampleRate == 44100)
			wavValide = true;
		else
			wavValide = false;
		
		return wavValide;
	}
	
	/**
	 * La méthode popData récupère les données du fichier d'entrée provenant
	 * de FileSource et les enregistrement dans certains attribut de la classe
	 * dans une liste.
	 */
	private void popData(){
		//Bloc TYPE
		tag1 = (new String(file.pop(4))).toCharArray();
		size1 = file.pop(4);
		format = (new String(file.pop(4))).toCharArray();
		
		//BLOC FORMAT
		tag2 = (new String(file.pop(4))).toCharArray();
		listeFormat.ajouter(file.pop(4));// size2
		listeFormat.ajouter(file.pop(2)); // Audio Format
		listeFormat.ajouter(file.pop(2)); // nbcanaux
		listeFormat.ajouter(file.pop(4)); // sampleRate
		listeFormat.ajouter(file.pop(4)); // byteRate
		listeFormat.ajouter(file.pop(2)); // BytePerBloc
		listeFormat.ajouter(file.pop(2)); // BitsPerSample

		//BLOC DONNÉES
		tag3 = (new String(file.pop(4))).toCharArray();
		size3 = file.pop(4);		
		listeDonnee = file.pop(convert2Int(size3,size3.length));
				
	}
	
	/**
	 * L'algorithme parcourt le tableau original 
	 * de données en sautant notre écart d'échantillon
	 * et va les mettres dans notre nouveau tableau de données.
	 * 
	 * */
	private void conversion(){

		int canal = convert2Int(listeFormat.getElement(2), 2); //nbCanaux
		int nbBits = convert2Int(listeFormat.getElement(6), 2); //bytePerSample
		int taille = convert2Int(size3, 4); //size3
		int blockAlign = convert2Int(listeFormat.getElement(5),2); //bitsPerBloc
		nouveaubyteRate = (int) (nouvelleFrequence * (nbBits/8) * canal);

		double ecart = SAMPLE_RATE/nouvelleFrequence;
		nouvelleTaille = (int) (taille/ecart);
		nouvelleDonnee = new byte[nouvelleTaille+1];
		
		
		int compteur = 0;
			
		for(double i=0;i<taille/blockAlign;i+=ecart)
		{
			for(int j=0;j<blockAlign;j++)
			{
				nouvelleDonnee[compteur] = listeDonnee[(int) (Math.round(i)*blockAlign)+j];
					
				compteur++;
			}			
		}
	}
	
	/**
	 * PushData renvoie les nouvelles donné à FileSink qui éffectue l'écriture
	 * sur le nouveau fichier. 
	 */
	private void pushData(){
		
		//BLOC TYPE
		out.push(new String(tag1).getBytes());
		out.push(size1);
		out.push(new String(format).getBytes());
		
		//BLOC FORMAT
		out.push(new String(tag2).getBytes());
		for(int i=0; i<listeFormat.taille(); i++)
			out.push(listeFormat.getElement(i));
		
		//BLOC DONNEES
		out.push(new String(tag3).getBytes());
		out.push(convert2Bytes(nouvelleTaille));
		out.push(nouvelleDonnee);
	}
	
	/**
	 * Convertir un tableau d'octect en int 
	 * 
	 * @param b: le tableau de donnée en octect
	 * @param size: la taille du tableau
	 * @return result: l'entier obtenu de la conversion. 
	 */
	private int convert2Int(byte[] b, int size){    

		int result = 0;

		if(size == 4){	    	 

			//comparaison, deplacements des bits et addition
			result = b[0] & MASK;
			result = result + ((b[1] & MASK) << 8);
			result = result + ((b[2] & MASK) << 16);
			result = result + ((b[3] & MASK) << 24);       
		}
		else if(size ==2){	    

			result = b[0] & MASK;
			result = result + ((b[1] & MASK) << 8);	               
		}

		return result;
	}	

	/**
	 * La méthode suivante reçoit en paramètre un entier et effectue la 
	 * conversion envers un tableau de byte en utilisant la classe ByteBuffer
	 * de la librairie.  
	 * 
	 * @param x, l'entier à convertir
	 * @return le tableau de bytes provenant des entiers convertis
	 */
	private byte[] convert2Bytes(int i)
	{
		byte[] result = new byte[4];

		result[3] = (byte) (i >> 24);
		result[2] = (byte) (i >> 16);
		result[1] = (byte) (i >> 8);
		result[0] = (byte) (i /*>> 0*/);

		return result;
	}
	
	/**
	 * Mise à jour des nouvelles données dans la liste
	 */
	private void updateDataList(){
		//Mise à jour de la nouvelle taille
		listeFormat.setElement(3,convert2Bytes((int)nouvelleFrequence));
		listeFormat.setElement(4,convert2Bytes(nouveaubyteRate));
		
	}
}
